package com.onadake.domain;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;

import com.onadake.utils.DomainUtils;
import com.onadake.utils.ValidationUtils;

/**
 * Entity object. This is a base object for everything.
 * This is expected to be translated into more appropriate objects.
 * 
 * @author radek.hecl
 *
 */
public final class Entity {

    /**
     * Builder object.
     */
    public static class Builder {

        /**
         * Code of the entity. This is unique identifier of each entity.
         */
        private String code;

        /**
         * Name of the entity. This is user friendly name, not mandatory.
         */
        private String name;

        /**
         * Date in the UTC time zone when entity has been created.
         * No matter in which zone this JVM is, the time will still be same as it is UTC.
         */
        private Date utcCreatedTimestamp;

        /**
         * Type of the entity;
         */
        private EntityType type;

        /**
         * Profile values of the entity.
         */
        private Map<String, String> profile = new HashMap<>();

        /**
         * Values from the joined tabled. Each key of the map has format table_name.column_name.
         */
        private Map<String, Object> joins = new HashMap<>();

        /**
         * Sets code of the entity. This is unique identifier.
         *  
         * @param code code of the entity
         * @return this instance
         */
        public Builder setCode(String code) {
            this.code = code;
            return this;
        }

        /**
         * Sets name of the entity.
         * 
         * @param name name of the entity
         * @return this instance
         */
        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        /**
         * Sets created timestamp as it is UTC time zone. No matter in what time zone this JVM is,
         * this will still point to the same day time.
         *  
         * @param utcCreatedTimestamp created timestamp as it is in the UTC time zone
         * @return this instance
         */
        public Builder setUtcCreatedTimestamp(Date utcCreatedTimestamp) {
            this.utcCreatedTimestamp = DomainUtils.copyDate(utcCreatedTimestamp);
            return this;
        }

        /**
         * Sets type of the entity.
         *  
         * @param type type of the entity
         * @return this instance
         */
        public Builder setType(EntityType type) {
            this.type = type;
            return this;
        }

        /**
         * Sets profile values of the entity.
         * 
         * @param profile profile values of the entity
         * @return this instance
         */
        public Builder setProfile(Map<String, String> profile) {
            this.profile = DomainUtils.softCopyMap(profile);
            return this;
        }

        /**
         * Adds profile property.
         * 
         * @param key key of the property
         * @param value value of the property
         * @return this instance
         */
        public Builder addProfile(String key, String value) {
            this.profile.put(key, value);
            return this;
        }

        /**
         * Sets values from the join tables of the entity. Each key in the map has format table_name.column_name.
         * 
         * @param joins values from join tables
         * @return this instance
         */
        public Builder setJoins(Map<String, Object> joins) {
            this.joins = DomainUtils.softCopyMap(joins);
            return this;
        }

        /**
         * Adds join table value.
         * 
         * @param key key of the property
         * @param value value of the property
         * @return this instance
         */
        public Builder addJoin(String key, Object value) {
            this.joins.put(key, value);
            return this;
        }

        /**
         * Builds the result object.
         * 
         * @return created object
         */
        public Entity build() {
            return new Entity(this);
        }
    }

    /**
     * Code of this entity. This is unique identifier of each entity.
     */
    private String code;

    /**
     * Name of this entity. This is user friendly name, not mandatory.
     */
    private String name;

    /**
     * Date in the UTC time zone when this entity has been created.
     * No matter in which zone this JVM is, the time will still be same as it is UTC.
     */
    private Date utcCreatedTimestamp;

    /**
     * Type of this entity;
     */
    private EntityType type;

    /**
     * Profile values of this entity.
     */
    private Map<String, String> profile;

    /**
     * Values from the joined tabled. Each key of the map has format table_name.column_name.
     */
    private Map<String, Object> joins;

    /**
     * Creates new instance.
     * 
     * @param builder builder object
     */
    public Entity(Builder builder) {
        code = builder.code;
        name = builder.name;
        utcCreatedTimestamp = DomainUtils.copyDate(builder.utcCreatedTimestamp);
        type = builder.type;
        profile = Collections.unmodifiableMap(DomainUtils.softCopyMap(builder.profile));
        joins = Collections.unmodifiableMap(DomainUtils.softCopyMap(builder.joins));
        guardInvariants();
    }

    /**
     * Guards this object to be consistent. Throws exception if this is not the case.
     */
    private void guardInvariants() {
        ValidationUtils.guardMatch(code, "[A-Z]{2}[0-9]{16}", "code must match [A-Z]{2}[0-9]{16}");
        ValidationUtils.guardNotNull(name, "name cannot be null");
        ValidationUtils.guardNotNull(utcCreatedTimestamp, "utcCreatedTimestamp cannot be null");
        ValidationUtils.guardNotNull(type, "type cannot be null");
        ValidationUtils.guardNotEmptyStringInCollection(profile.keySet(), "profile cannot have empty key");
        ValidationUtils.guardNotNullCollection(profile.values(), "profile cannot have null value");
        ValidationUtils.guardNotEmptyStringInCollection(joins.keySet(), "joins cannot have empty key");
        ValidationUtils.guardNotNullCollection(joins.values(), "joins cannot have null value");
    }

    /**
     * Returns code of this entity. This is the unique identifier.
     * 
     * @return code of this entity as a unique identifier
     */
    public String getCode() {
        return code;
    }

    /**
     * Returns name of this entity.
     * 
     * @return name of this entity
     */
    public String getName() {
        return name;
    }

    /**
     * Returns date in the UTC time zone when this entity has been created.
     * No matter in which zone this JVM is, the time will still be same as it is UTC.
     * 
     * @return date in the UTC time zone when this entity has been created
     */
    public Date getUtcCreatedTimestamp() {
        return utcCreatedTimestamp;
    }

    /**
     * Returns type of this entity.
     * 
     * @return type of this entity
     */
    public EntityType getType() {
        return type;
    }

    /**
     * Returns profile of this entity.
     * 
     * @return profile of this entity
     */
    public Map<String, String> getProfile() {
        return profile;
    }

    /**
     * Returns joins of this entity. Each key of the result has format table_name.column_name.
     * 
     * @return joins of this entity
     */
    public Map<String, Object> getJoins() {
        return joins;
    }

    /**
     * Returns the sorted joins. Key of the outer map is a table and value is the column name value pair.
     * 
     * @return sorted joins
     */
    public Map<String, Map<String, Object>> getSortedJoins() {
        Map<String, Map<String, Object>> res = new HashMap<>();
        for (Map.Entry<String, Object> entry : joins.entrySet()) {
            String[] parts = entry.getKey().split("\\.", 2);
            if (!res.containsKey(parts[0])) {
                res.put(parts[0], new HashMap<String, Object>());
            }
            res.get(parts[0]).put(parts[1], entry.getValue());
        }
        return res;
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

    @Override
    public boolean equals(Object obj) {
        return EqualsBuilder.reflectionEquals(this, obj);
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

}
